#  mpdconnector.py - MPD connector modules
#  Copyright (C) 2005-2007 Sham Chukoury <eleusis@xmms.org>
#
#  This library is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2.1 of the License, or (at your option) any later version.
#
#  This library is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#  Lesser General Public License for more details.

import Chalyx
from Chalyx.base import ChalyxObj
import difflib

class MPDStateDict(dict):
	def __init__(self, watchedKeys):
		"""
		watchedKeys - dictionary with key names to watch for,
		in this dict. Each key in watchedKeys references a
		callback to be executed when the value associated
		to a key in StateDict is modified.
		"""
		self.watchedKeys = watchedKeys
		self._dispatch = []

	def __setitem__(self, key, val):
		oldVal = None
		notify = False
		if self.has_key(key) and self[key] != val:
			oldVal = self[key]
			dict.__setitem__(self, key, val)
			notify = True
		elif not self.has_key(key):
			dict.__setitem__(self, key, val)
			notify = True
		if notify and key in self.watchedKeys.keys():
			self._dispatch.append((key, oldVal, val))

	def update(self, newDict):
		# override dict.update because it doesn't call __setitem__
		for key, value in newDict.items():
			self[key] = value
		while len(self._dispatch) > 0:
			cbdata = self._dispatch.pop(0)
			self.watchedKeys[cbdata[0]](self, cbdata[0], cbdata[1], cbdata[2])

class MPDConnector(ChalyxObj):
	def __init__(self, mpd, pingInterval = 5, args = None):
		ChalyxObj.__init__(self, mpd.cfgHandler)
		self.config = self.cfgHandler["MPDConnector"]
		self.config.register("plsChange", "Algorithm to use to process playlist changes", "plchanges", ["plchanges", "pldiff"])
		self.mpd = mpd
		self.pingInterval = pingInterval
		self.statusTimeout = 500
		self.mpdState = MPDStateDict({
			"state": self.on_pbstatus_change,
			"time": self.on_playtime_change,
			"song": self.on_song_change,
			"playlist": self.on_playlist_change
		})
		self.signals = {
			"pb_playtime": [],
			"pb_status": [],
			"pb_current_id": [],
			"pl_pos": [],
			"pl_change": []
		}
		self.pbstatus_map = {
			"stop": Chalyx.STATUS_STOPPED,
			"play": Chalyx.STATUS_PLAYING,
			"pause": Chalyx.STATUS_PAUSED
		}
		self.pls = []
	
	def stop(self):
		# called when disconnecting the service
		if self.mpd.connected:
			self.mpd.close()

	def on_pbstatus_change(self, stateDict, key, oldStatus, status):
		#print "pb status changed from %s to %s" % (oldStatus, status)
		if oldStatus is None: return
		cStatus = Chalyx.STATUS_UNKNOWN
		if self.pbstatus_map.has_key(status):
			cStatus = self.pbstatus_map[status]
		self.emit("pb_status", cStatus)

	def on_playtime_change(self, stateDict, key, oldPtime, playtime):
		self.emit("pb_playtime", float(playtime.split(":", 1)[0]) * 1000)

	def on_song_change(self, stateDict, key, oldSong, song):
		self.emit("pl_pos", song)
		self.emit("pb_current_id", self.pls[int(song)])
	
	def on_playlist_change(self, stateDict, key, oldPlsV, plsV):
		# use this if it's trivial to remove and replace a large number of
		# items in the frontend
		def __plchanges_cb(changes):
			oldPls = self.pls[:]
			plsLen = int(self.mpdState["playlistlength"])
			if plsLen > len(oldPls):
				self.pls += [None] * (plsLen - len(oldPls))
			elif plsLen < len(oldPls):
				while len(self.pls) > plsLen:
					self.pls.pop()
					chg = {
						"type": Chalyx.PLAYLIST_CHANGED_REMOVE,
						"position": len(self.pls) - 1
					}
					self.emit("pl_change", chg)
			for chg in changes:
				# mpd usually sends plchanges in playlist order, so we
				# should always be able to insert at end..
				self.pls[int(chg["pos"])] = chg["file"]
				chg = {
					"type": Chalyx.PLAYLIST_CHANGED_INSERT,
					"position": int(chg["pos"]),
					"id": chg["file"]
				}
				self.emit("pl_change", chg)
		
		# use this if it's better to only make minimal changes to the frontend
		# store
		def __pldiff_cb(changes):
			# here we know about songs which have changed positions...
			# or is that positions which have changed songs..?
			oldPls = self.pls[:]
			# build new playlist from plchanges data
			plsLen = int(self.mpdState["playlistlength"])
			if plsLen > len(oldPls):
				self.pls += [None] * (plsLen - len(oldPls))
			elif plsLen < len(oldPls):
				while len(self.pls) > plsLen:
					self.pls.pop()
			for chg in changes:
				self.pls[int(chg["pos"])] = chg["file"]
			
			# diff the playlists
			differ = difflib.Differ()
			pos = 0
			for line in differ.compare(oldPls, self.pls):
				if line.startswith("-"):
					chg = {
						"type": Chalyx.PLAYLIST_CHANGED_REMOVE,
						"position": pos
					}
					self.emit("pl_change", chg)
					pos -= 1
				elif line.startswith("+"):
					if pos >= len(oldPls):
						chg = {
							"type": Chalyx.PLAYLIST_CHANGED_ADD,
							"id": self.pls[pos]
						}
					else:
						chg = {
							"type": Chalyx.PLAYLIST_CHANGED_INSERT,
							"position": pos,
							"id": self.pls[pos]
						}
					self.emit("pl_change", chg)
				pos += 1
		if oldPlsV is None: return
		if int(self.mpdState["playlistlength"]) == 0:
			# playlist cleared, just emit and return, nothing further to do
			self.pls = []
			self.emit("pl_change", {"type": Chalyx.PLAYLIST_CHANGED_CLEAR})
			return
		if self.config["plsChange"] == "pldiff":
			self.mpd.plchanges(int(oldPlsV), cb=__pldiff_cb)
		else:
			self.mpd.plchanges(int(oldPlsV), cb=__plchanges_cb)
	
	def status_check(self, initial=False):
		def __status(mpdStatus):
			self.mpdState.update(mpdStatus)
		def __pls_cb(data):
			self.pls = data
		if not self.mpd.connected: return
		if initial:
			self.mpd.playlist_list(cb=__pls_cb)
		self.mpd.status(cb=__status)
		return True

	# make sure we use Chalyx signals by default
	def connect(self, signame, cb, udata):
		Spirit.connect(self, signame, cb, udata)

	def disconnect(self, sigtoken):
		Spirit.disconnect(self, sigtoken)

	def emit(self, signame, sigdata):
		Spirit.emit(self, signame, sigdata)
